<?php
if (!class_exists('AlfaBeanLoader')) {

	require_once 'lib/addendum/annotations.php';

	/**
	 * AlfaBeanLoader Class
	 * Singleton. Manages class/interface retrieval based on a gived ID through annotations instead of class name.
	 * @package loader
	 * @author Alecsandru Gabriel Dinu
	 */
	class AlfaBeanLoader implements Singleton_Interface {
		private static $theInstance;
		private static $cache;

		/**
		 * Constructor
		 * Initialize SmartLoader
		 * @access private
		 */
		private function __construct() {
			if (!isset($this->cache)) {
				$cacheManager = AlfaCacheManager::getInstance();
				if ($cacheManager->isEternalElement("alfa_beans")) {
					$this->cache = $cacheManager->getEternalElement("alfa_beans");
				}
				if (!isset($this->cache)) {
					$this->load();
				}
			}
		}

		/**
		 * Static factory method
		 * @access public
		 */
		public static function getInstance() {
			if (!self::$theInstance) {
				self::$theInstance = new AlfaBeanLoader();
			}
			return self::$theInstance;
		}

		/**
		 * Gets the list of existing classes, and reads the annotations
		 * @access private
		 */
		private function load() {
			$classes = AlfaLoader::getInstance()->listClasses();
			foreach ($classes as $classname => $filename) {
				if (!$this->isInterface($classname)) {
					$reflection = new ReflectionAnnotatedClass($classname); // by class name
					if ($reflection->hasAnnotation('Bean')) {
						$annotation = $reflection->getAnnotation('Bean');

						if (!isset($annotation->id)) {
							throw new BeansException("Invalid bean definition, 'id' attribute is missing.");
						}

						// load bean properties
						$properties = array();
						if ($reflection->hasAnnotation('Property')) {
							$just_props = $reflection->getAllAnnotations('Property');
							foreach($just_props as $property) {
								if(!isset($property->name)) {
									throw new PropertyAccessException("Property name is missing for bean " . $annotation->id);
								}

								if(isset($property->ref)) {
									$type = "ref";
								} else if(isset($property->value)) {
									$type = "value";
								} else {
									throw new PropertyAccessException("Invalid property " . $property->name);
								}

								$properties[$property->name] = array(
									"type" => $type,
									"value" => $property->value,
									"ref" => $property->ref
								);
							}
						}

						$beans[$annotation->id] = array (
							"class" => $classname,
							"scope" => $annotation->scope,
							"factory_method" => $annotation->factory_method,
							"init_method" => $annotation->init_method,
							"properties" => $properties
						);
					}
				}
			}
			$cacheManager = AlfaCacheManager::getInstance();
			$cacheManager->putEternalElement("alfa_beans", $beans);
				
			$this->cache = $cacheManager->getEternalElement("alfa_beans");
		}

		/**
		 * Returns a declared bean class instance
		 * @param string
		 * @return class instance
		 * @throws NoSuchBeanDefinitionException
		 * @throws BeanCreationException
		 * @access public
		 */
		public function getBean($id) {
			if (!isset($this->cache[$id])) {
				throw new NoSuchBeanDefinitionException("Bean $id was not find.");
			}

			$scope = "auto";
			if (!isset($this->cache[$id]["scope"])) {
				$scope = $this->cache[$id]["scope"];
			}
				
			// Create an instance of the ReflectionClass class
			$reflection = new ReflectionClass($this->cache[$id]["class"]);
				
			// If this class is instantiable, create an instance
			if ($reflection->isInstantiable()) {

				if ($scope == "auto") {
					$scope = "prototype";
					$interfaces = $reflection->getInterfaces();
					foreach($interfaces as $in) {
						if ($in->getName() == "Singleton_Interface") {
							$scope = "singleton";
						}
					}
				}

				if ($scope == "prototype") {
					$bean = $reflection->newInstance();
				} else if ($scope == "singleton") {
					$method = new ReflectionMethod($this->cache[$id]["class"], $this->cache[$id]["factory_method"]);
					$bean = $method->invoke(NULL);
				}

				if ($reflection->isInstance($bean)) {
					// try to load properties
					$props = $this->cache[$id]["properties"];
					foreach($props as $propname => $prop) {
						if ($prop["type"] == "value") { // value property
							$parameter = $prop["value"];
						} else { // reference property
							$parameter = $this->getBean($prop["ref"]);
						}
						$method_name = "set".ucwords($propname);
						$method = new ReflectionMethod($this->cache[$id]["class"], $method_name);

						$method->invoke($bean, $parameter);

					}
						
					// check if a init method was defined and call it if necessary
					if(StringUtils::isNotBlank($this->cache[$id]["init_method"])) {
						$method = new ReflectionMethod($this->cache[$id]["class"], $this->cache[$id]["init_method"]);
						$method->invoke($bean);
					}
						
					return $bean;
				}
			}
				
			throw new BeanCreationException("Could not create instance of bean with id" . $id);
		}

		/**
		 * Returns only the bean definition array
		 * @param string
		 * @access public
		 * @return array
		 */
		public function getBeanDefinition($id) {
			if (!isset($this->cache[$id])) {
				throw new NoSuchBeanDefinitionException("Bean $id was not find.");
			}
			return $this->cache[$id];
		}

		/**
		 * Returns a reflection class of the specified bean id
		 * @param string
		 * @access public
		 * @return ReflectionClass
		 */
		public function getBeanReflection($id) {
			if (!isset($this->cache[$id])) {
				throw new NoSuchBeanDefinitionException("Bean $id was not find.");
			}
			// Create an instance of the ReflectionClass class
			$reflection = new ReflectionClass($this->cache[$id]["class"]);
			return $reflection;
		}

		/**
		 * Check if the name coresponds to an interface
		 * @return boolean
		 * @access private
		 */
		private function isInterface($classname) {
			$class = new ReflectionClass($classname);
			return $class->isInterface();
		}

		/**
		 * Override clone method to throw exception
		 * @access public
		 * @return void
		 * @throws Exception
		 */
		public function __clone() {
			throw new Exception('You cannot clone singleton object');
		}
	}
}
